spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { Bars } from '@/components/charts/bars';5import { LineChart } from '@/components/charts/line-chart';6import { WorldMap } from '@/components/charts/world-map';7import { CompanyMiniList, CompanyTable } from '@/components/company/company-table';8import { EventList } from '@/components/events/event-row';9import { CountryChip } from '@/components/ui/badges';10import { Container, Empty, Note, PageHeader, Section, Stat, StatGrid } from '@/components/ui/section';11import { api, ApiError, safe } from '@/lib/api';12import { fmtInt, fmtPctSigned, fmtScore } from '@/lib/format';13import { routes } from '@/lib/site';14import type { CompanyCard, CountryDetailRaw } from '@/lib/types';1516export const revalidate = 300;1718async function load(code: string): Promise<CountryDetailRaw> {19 try {20 return await api.country(code.toUpperCase());21 } catch (e) {22 if (e instanceof ApiError && e.notFound) notFound();23 throw e;24 }25}26export async function generateMetadata({ params }: { params: Promise<{ code: string }> }): Promise<Metadata> {27 const { code } = await params;28 const d = await safe(api.country(code.toUpperCase()));29 if (!d) return { title: 'Country' };30 return { title: `${d.name} — country atlas`, description: `${d.name}: ${Array.isArray(d.companies) ? d.companies.length : d.companies} monitored companies, ${d.events_30d} structured events in 30 days, hiring momentum ${fmtPctSigned(d.hiring_momentum_30d)}.`, alternates: { canonical: `/country/${code.toLowerCase()}` } };31}3233export default async function CountryPage({ params }: { params: Promise<{ code: string }> }) {34 const { code } = await params;35 const d = await load(code);36 const companies: CompanyCard[] = Array.isArray(d.companies) ? d.companies : [];37 const count = Array.isArray(d.companies) ? d.companies.length : d.companies;38 const map = await safe(api.map('events_30d'));39 const buckets = (map?.buckets ?? []).filter((b) => b.country.toUpperCase() === d.code.toUpperCase());40 return (41 <Container wide>42 <PageHeader43 eyebrow={44 <>45 <Link href={routes.countries()} className="hover:text-ink">46 Country atlas47 </Link>48 <span>/</span>49 <CountryChip code={d.code} link={false} />50 {d.region && <span>{d.region}</span>}51 </>52 }53 title={d.name}54 lede={`${fmtInt(count)} monitored companies headquartered in ${d.name}. Activity and hiring are averages over the monitored population.`}55 />56 <StatGrid cols={5}>57 <Stat label="Companies" value={fmtInt(count)} size="sm" />58 <Stat label="Events 7 d" value={fmtInt(d.events_7d)} size="sm" />59 <Stat label="Events 30 d" value={fmtInt(d.events_30d)} size="sm" />60 <Stat label="Activity" value={fmtScore(d.activity_score)} size="sm" />61 <Stat label="Hiring 30 d" value={fmtPctSigned(d.hiring_momentum_30d)} size="sm" />62 </StatGrid>63 <div className="grid gap-8 lg:grid-cols-12">64 <Section eyebrow="Activity" title="Country activity, 90 days" className="lg:col-span-8">65 {d.series?.length > 1 ? <LineChart series={[{ id: 'activity', label: 'Activity score', points: d.series.map((p) => ({ day: p.day, value: p.value })) }]} height={200} yZero /> : <Empty compact title="Not enough history yet." />}66 </Section>67 <Section eyebrow="Industry mix" title="Monitored companies by industry" className="lg:col-span-4">68 {d.industry_mix?.length ? <Bars dense rows={d.industry_mix.slice(0, 10).map((m) => ({ key: m.industry, label: m.industry.replace(/-/g, ' '), value: m.companies, href: routes.industry(m.industry) }))} /> : <Empty compact />}69 </Section>70 </div>71 <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">72 <Section eyebrow="Top movers" title="Highest Corporate Change Index">73 <CompanyMiniList items={d.movers ?? []} metric="corporate_change_index" label="CCI" sparkline />74 </Section>75 <Section eyebrow="New entrants" title="Most recently onboarded">76 <CompanyMiniList items={d.new_entrants ?? []} metric="activity_score" label="activity" />77 </Section>78 <Section eyebrow="Map" title="Headquarters clusters">79 {buckets.length ? <WorldMap buckets={buckets} highlight={[d.code]} interactive={false} /> : <Empty compact title="No geocoded clusters yet." />}80 </Section>81 </div>82 <Section eyebrow="Companies" title="Most active monitored companies" action={{ href: routes.companies({ country: d.code }), label: 'All in directory' }}>83 <CompanyTable items={companies} />84 </Section>85 <Section eyebrow="Events" title="Latest structured events" action={{ href: routes.events({ country: d.code }), label: 'All events' }}>86 <EventList events={d.events ?? []} variant="table" />87 <Note className="mt-3">Country attribution uses the company’s headquarters. Expansion events (new offices, new countries) appear under the company’s home country and on the destination country’s timeline where detected.</Note>88 </Section>89 </Container>90 );91}92